#include "XmlSocket.hh"
#include "../Exception.hh"

using namespace Network;

/**
 * \brief Statikus mutex, az XML értelmező miatt van rá szükség.
 */
QMutex XmlSocket::staticLock;

// TODO ne server init errort dobjon, mert a kliens is használhatja
QSslConfiguration
XmlSocket::ssl_config(
  QString const& keyPath,
  QString const& certPath,
  QString const& caPath
) {
    QSslConfiguration sslConfig;
    { // Titkos kulcs
        QFile keyFile(keyPath);

        if (!keyFile.open(QIODevice::ReadOnly))
            throw ServerInitError("Error opening private key");

        QSslKey key(&keyFile, QSsl::Rsa); // TODO titkosított kulcsok támogatása
        keyFile.close();

        if (key.isNull())
            throw ServerInitError("Error reading private key");

        sslConfig.setPrivateKey(key);
    }
    { // Saját tanúsítvány
        QFile certFile(certPath);

        if (!certFile.open(QIODevice::ReadOnly))
            throw ServerInitError("Error opening local certificate");

        QSslCertificate cert(&certFile);
        certFile.close();

        if (cert.isNull())
            throw ServerInitError("Error reading local certificate");

        sslConfig.setLocalCertificate(cert);
    }
    { // CA tanúsítvány
        QFile caFile(caPath);

        if (!caFile.open(QIODevice::ReadOnly))
            throw ServerInitError("Error opening CA certificate");

        QSslCertificate caCert(&caFile);
        caFile.close();

        if (caCert.isNull())
            throw ServerInitError("Error reading CA certificate");

        QList< QSslCertificate > caCertList;
        caCertList.append(caCert);
        sslConfig.setCaCertificates(caCertList);
    }
    sslConfig.setPeerVerifyMode(QSslSocket::VerifyPeer);
    sslConfig.setProtocol(QSsl::SslV3);
    //setCiphers(); // TODO: supportedCiphers-ből kiválogatni 1-2-t
    return sslConfig;
}

/**
 * \brief Konstruktor.
 *
 * Csatlakoztatja a QSslSocket-től örökölt readyRead() signalt a read_data()
 * slothoz.
 */
XmlSocket::XmlSocket(QObject* parent, QSslConfiguration const& sslConfig):
    QSslSocket(parent),
    lock(QMutex::Recursive),
    size(0),
    inputBuffer(),
    messageBuffer()
{
    setSslConfiguration(sslConfig);
    connect(this, SIGNAL(encrypted()), this, SLOT(connection_established()));
    connect(this, SIGNAL(readyRead()), this, SLOT(read_data()));
}

/**
 * \brief Üzenet fogadása.
 *
 * Csak akkor szabad meghívni, ha a messages_available() igaz értéket ad.
 */
QDomDocument XmlSocket::read() {
    QMutexLocker locker(&lock);
    QDomDocument message = messageBuffer.dequeue();
    QDomElement root = message.documentElement();
    qDebug(
      "%s:%d <- %s:%d    %s.%s",
      localAddress().toString().toStdString().c_str(),
      localPort(),
      peerAddress().toString().toStdString().c_str(),
      peerPort(),
      root.tagName().toStdString().c_str(),
      root.firstChildElement().tagName().toStdString().c_str()
    );
    return message;
}

/**
 * \brief Üzenet elküldése.
 * \param message Az üzenet.
 *
 * Vár, amíg a teljes üzenet el nem lett küldve.
 */
void XmlSocket::write(QDomDocument& message) {
    QMutexLocker locker(&lock);
    QByteArray data = message.toByteArray();
    uint64_t size = data.length();
    QSslSocket::write(
      QByteArray(reinterpret_cast< char* >(&size), sizeof(uint64_t)) + data
    );

    if (state() == QAbstractSocket::ConnectedState)
        waitForBytesWritten(-1);

    QDomElement root = message.documentElement();
    qDebug(
      "%s:%d -> %s:%d    %s.%s",
      localAddress().toString().toStdString().c_str(),
      localPort(),
      peerAddress().toString().toStdString().c_str(),
      peerPort(),
      root.tagName().toStdString().c_str(),
      root.firstChildElement().tagName().toStdString().c_str()
    );
}

/// \brief Van-e azonnal kiolvasható üzenet.
bool XmlSocket::messages_available() {
    QMutexLocker locker(&lock);
    bool result = !messageBuffer.isEmpty();
    return result;
}

/**
 * \brief Egy üzenetet beolvasó függvény.
 *
 * Beolvas egy teljes üzenetet. Ha nem érkezett elég adat, megvárja, amíg megjön
 * az egész üzenet, majd a kapott üzenetből QDomDocument-et készít, amit
 * beletesz a messageBuffer-be. Hibásan formázott XML esetén bontja a
 * kapcsolatot.
 */
void XmlSocket::read_message() {
    QMutexLocker locker(&lock);

    if (size == 0) {
        // Üzenet eleje
//    	qDebug("e %d", localPort());
        QByteArray sizeBytes = QSslSocket::read(sizeof(u8));
        size = *reinterpret_cast< u8* >(sizeBytes.data());
    }

    i8 bufSize = inputBuffer.size();

    if (bufSize < i8(size)) {
//    	qDebug("k %d", localPort());
        // Üzenet közepe
        inputBuffer += QTcpSocket::read(size - bufSize);
    }

    if (inputBuffer.size() >= i8(size)) {
//    	qDebug("v %d", localPort());
        // Üzenet vége
        QDomDocument doc;
        {
            QMutexLocker sLocker(&staticLock);

            if (!doc.setContent(inputBuffer)) {
                qDebug("Bad XML");
                disconnectFromHost();
            }

        }
        messageBuffer.enqueue(doc);
        size = 0;
        inputBuffer.clear();
        emit message_recieved();
    }
}

/**
 * \brief Kapcsolat kiépítését jelző slot
 */
void XmlSocket::connection_established() {
//	QString username = localCertificate().subjectInfo(
//    		QSslCertificate::CommonName);
//    QString servername = peerCertificate().subjectInfo(
//    		QSslCertificate::CommonName);
//
//    qDebug("Connected to server %s as %s", servername.toStdString().c_str(),
//    	username.toStdString().c_str());
}

/**
 * \brief Adat beolvasásával foglalkozó slot.
 *
 * Addig hívja a read_message() függvényt, amíg van annyi olvasásra váró
 * adat, amiből az üzenet hossza kiderül. Ha nem érkezett elég adat, nem vár
 * többre, úgyis meg lesz hívva, mikor jön még.
 */
void XmlSocket::read_data() {
    while (
      static_cast<uint64_t>(bytesAvailable()) >= sizeof(uint64_t)
    ) {
        read_message();
    }
}

/**
 * \brief Hibakezeléssel foglalkozó slot
 */
void XmlSocket::errorOccured(const QList<QSslError> & error) {
	// TODO: megfelelő hibakezelés
	qDebug("SSL connection error");
	QList<QSslError>::const_iterator i;
	for (i = error.begin(); i != error.end(); ++i) {
		qDebug("%s", (*i).errorString().toStdString().c_str());
	}
	ignoreSslErrors();
}

